/*******************************************************************************
 * Copyright 2010 Simon Mieth
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
/*
 * Created on 13.04.2005
 *
 */
package cn.kkdlk.parse.entities;

import cn.kkdlk.parse.DraftDocument;
import cn.kkdlk.parse.common.Type;
import cn.kkdlk.parse.math.*;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;


/**
 * @author <a href="mailto:simon.mieth@gmx.de>Simon Mieth</a>
 */
public class LWPolyline extends Entity {


    protected static final double QUARTER_CIRCLE_ANGLE = Math.tan(0.39269908169872414D);
    private final static int LAZY_INDEX_CONSTANTWIDTH = 10;
    private final static int LAZY_INDEX_STARTWIDTH = 11;
    private final static int LAZY_INDEX_ENDWIDTH = 12;
    private final static int LAZY_INDEX_ELEVATION = 13;
    private final static int BOOLEAN_BIT_CONSTANTWIDTH = 10;
    protected List<LW2DVertex> vertices = new ArrayList<LW2DVertex>();

    public LWPolyline() {
    }

    public double getContstantWidth() {
        if (this.lazyContainer.contains(LAZY_INDEX_CONSTANTWIDTH)) {
            return ((Double) this.lazyContainer.get(LAZY_INDEX_CONSTANTWIDTH)).doubleValue();
        }
        return 0.0;
    }

    public Type<LWPolyline> getType() {
        return Type.TYPE_LWPOLYLINE;
    }

    public Bounds getBounds() {
        Bounds bounds = new Bounds();

        Iterator<LW2DVertex> i = vertices.iterator();

        if (i.hasNext()) {
            LW2DVertex last;
            LW2DVertex first;
            LW2DVertex v = null;

            last = first = i.next();
            bounds.addToBounds(last.getX(), last.getY(), 0.0);

            while (i.hasNext()) {
                v = i.next();
                addToBounds(last, v, bounds);
                last = v;
            }

            if ((v != null) && (v.getBulge() != 0.0)) {
                addToBounds(v, first, bounds);
            }
        } else {
            bounds.setValid(false);
        }

        return bounds;
    }

    public void addVertex(LW2DVertex vertex) {
        vertices.add(vertex);
    }

    public int getVertexCount() {
        return this.vertices.size();
    }

    public List<LW2DVertex> getVertices() {
        return this.vertices;
    }

    public void removeVertex(LW2DVertex vertex) {
        // remove and check the constantwidth
        this.setBit(BOOLEAN_BIT_CONSTANTWIDTH, true);
        for (Iterator<LW2DVertex> i = this.vertices.iterator(); i.hasNext(); ) {
            LW2DVertex v = i.next();
            if (v == vertex) {
                i.remove();
            } else if (!v.isConstantWidth()) {
                this.setBit(BOOLEAN_BIT_CONSTANTWIDTH, false);
            }
        }
    }

    public void removeVertex(int index) {
        this.setBit(BOOLEAN_BIT_CONSTANTWIDTH, true);
        int count = 0;
        for (Iterator<LW2DVertex> i = this.vertices.iterator(); i.hasNext(); ) {
            LW2DVertex v = i.next();
            if (count == index) {
                i.remove();
            } else if (!v.isConstantWidth()) {
                this.setBit(BOOLEAN_BIT_CONSTANTWIDTH, false);
            }
            count++;
        }
    }

    public LW2DVertex getVertex(int i) {
        return vertices.get(i);
    }

    /**
     * Caculate the radius of a cut circle segment between 2 Vertex
     *
     * @param bulge  the vertex bulge
     * @param length the length of the circle cut
     */
    public double getRadius(double bulge, double length) {
        double h = (bulge * length) / 2;
        double value = (h / 2) + (Math.pow(length, 2) / (8 * h));

        return Math.abs(value);
    }

    /**
     * @return Returns the endWidth.
     */
    public double getEndWidth() {
        if (this.lazyContainer.contains(LAZY_INDEX_ENDWIDTH)) {
            return ((Double) this.lazyContainer.get(LAZY_INDEX_ENDWIDTH)).doubleValue();
        }
        return 0.0;
    }

    /**
     * @param endWidth The endWidth to set.
     */
    public void setEndWidth(double endWidth) {
        if (endWidth != 0.0) {
            this.lazyContainer.set(new Double(endWidth), LAZY_INDEX_ENDWIDTH);
        }
    }

    /**
     * @return Returns the startWidth.
     */
    public double getStartWidth() {
        if (this.lazyContainer.contains(LAZY_INDEX_STARTWIDTH)) {
            return ((Double) this.lazyContainer.get(LAZY_INDEX_STARTWIDTH)).doubleValue();
        }
        return 0.0;
    }

    /**
     * @param startWidth The startWidth to set.
     */
    public void setStartWidth(double startWidth) {
        if (startWidth != 0.0) {
            this.lazyContainer.set(new Double(startWidth), LAZY_INDEX_STARTWIDTH);
        }
    }

    public boolean isClosed() {
        // the closed Flag
        return (this.flags & 1) == 1;
    }

    public void setClosed(boolean b) {
        if (b) {
            this.flags = this.flags | 1;
        } else if (this.isClosed()) {
            this.flags = this.flags ^ 1;
        }
    }

    public boolean isConstantWidth() {
        //TODO review to see if the
        //property is always set correct
        if (!isBitEnabled(BOOLEAN_BIT_CONSTANTWIDTH)) {
            return false;
        } else {
            setBit(BOOLEAN_BIT_CONSTANTWIDTH, true);

            Iterator<LW2DVertex> i = vertices.iterator();

            while (i.hasNext()) {
                LW2DVertex vertex = i.next();

                if (!vertex.isConstantWidth()) {
                    setBit(BOOLEAN_BIT_CONSTANTWIDTH, false);

                    return isBitEnabled(BOOLEAN_BIT_CONSTANTWIDTH);
                }
            }
        }

        return isBitEnabled(BOOLEAN_BIT_CONSTANTWIDTH);
    }

    public void setConstantWidth(double width) {
        if (width != 0.0) {
            this.lazyContainer.set(new Double(width), LAZY_INDEX_CONSTANTWIDTH);
        }
    }

    protected void addToBounds(LW2DVertex start, LW2DVertex end, Bounds bounds) {
        if (start.getBulge() != 0) {
            // calculte the height

            double l = MathUtils.distance(start.getPoint(), end.getPoint());

            // double h = Math.abs(last.getBulge()) * l / 2;
            double r = this.getRadius(start.getBulge(), l);

            double s = l / 2;
            Vector edgeDirection = MathUtils.getVector(start.getPoint(),
                    end.getPoint());
            edgeDirection = MathUtils.normalize(edgeDirection);

            Point3D centerPoint = MathUtils.getPointOfStraightLine(start.getPoint(),
                    edgeDirection, s);

            Vector centerPointDirection = MathUtils.crossProduct(edgeDirection,
                    this.getExtrusion().getNormal());
            centerPointDirection = MathUtils.normalize(centerPointDirection);

            // double t = Math.sqrt(Math.pow(r, 2) - Math.pow(s, 2));
            // double t = 0;
            double h = Math.abs(start.getBulge() * l) / 2;

            // if(Math.abs(start.getBulge())>=1.0){
            // t = h-r;
            // }else{
            // //t = Math.sqrt(Math.pow(r, 2) - Math.pow(s, 2));
            // t=r-h;
            // }
            // the center point of the arc
            int startQ = 0;
            int endQ = 0;

            double bulge = start.getBulge();

            if (bulge > 0) {
                // the arc goes over the right side, but where is the center
                // point?
                if (bulge > 1.0) {
                    double t = h - r;
                    centerPoint = MathUtils.getPointOfStraightLine(centerPoint,
                            centerPointDirection, t);
                } else {
                    double t = r - h;
                    centerPoint = MathUtils.getPointOfStraightLine(centerPoint,
                            centerPointDirection, (-1 * t));
                }

                endQ = MathUtils.getQuadrant(end.getPoint(), centerPoint);
                startQ = MathUtils.getQuadrant(start.getPoint(), centerPoint);
            } else {
                // the arc goes over the left side, but where is the center
                // point?
                if (bulge < -1.0) {
                    double t = h - r;
                    centerPoint = MathUtils.getPointOfStraightLine(centerPoint,
                            centerPointDirection, (-1 * t));
                } else {
                    double t = r - h;
                    centerPoint = MathUtils.getPointOfStraightLine(centerPoint,
                            centerPointDirection, t);
                }

                startQ = MathUtils.getQuadrant(end.getPoint(), centerPoint);
                endQ = MathUtils.getQuadrant(start.getPoint(), centerPoint);
            }

            if (endQ < startQ) {
                endQ += 4;
            } else if ((endQ == startQ) &&
                    (Math.abs(start.getBulge()) > QUARTER_CIRCLE_ANGLE)) {
                endQ += 4;
            }

            while (endQ > startQ) {
                switch (startQ) {
                    case 0:
                        bounds.addToBounds(centerPoint.getX(),
                                centerPoint.getY() + r, centerPoint.getZ());

                        break;

                    case 1:
                        bounds.addToBounds(centerPoint.getX() - r,
                                centerPoint.getY(), centerPoint.getZ());

                        break;

                    case 2:
                        bounds.addToBounds(centerPoint.getX(),
                                centerPoint.getY() - r, centerPoint.getZ());

                        break;

                    case 3:
                        bounds.addToBounds(centerPoint.getX() + r,
                                centerPoint.getY(), centerPoint.getZ());
                        endQ -= 4;
                        startQ -= 4;

                        break;
                }

                startQ++;
            }
        }

        bounds.addToBounds(start.getPoint());
        bounds.addToBounds(end.getPoint());
    }

    public double getLength() {
        double length = 0.0;

        // a normal polyline with or without bulges
        Iterator<LW2DVertex> i = this.vertices.iterator();
        LW2DVertex first;
        LW2DVertex last = first = i.next();

        while (i.hasNext()) {
            LW2DVertex v = i.next();
            length += this.getSegmentLength(last, v);
            last = v;
        }

        if (this.isClosed()) {
            length += this.getSegmentLength(last, first);
        }


        return length;
    }

    protected double getSegmentLength(LW2DVertex start, LW2DVertex end) {
        double l = MathUtils.distance(start.getPoint(), end.getPoint());

        if (start.getBulge() == 0.0) {
            return l;
        } else {
            double alpha = 4 * Math.atan(Math.abs(start.getBulge()));

            double r = l / (2 * Math.sin(alpha / 2));
            double d = (Math.PI * Math.toDegrees(alpha) * r) / 180;

            return d;
        }
    }

    /**
     * @return the elevation
     */
    public Point3D getElevation() {
        if (this.lazyContainer.contains(LAZY_INDEX_ELEVATION)) {
            return (Point3D) this.lazyContainer.get(LAZY_INDEX_ELEVATION);
        }

        return new Point3D() {
            public void setX(double x) {
                this.addToContainer();
                super.setX(x);
            }

            public void setY(double y) {
                this.addToContainer();
                super.setY(y);
            }

            public void setZ(double z) {
                this.addToContainer();
                super.setZ(z);
            }

            private void addToContainer() {
                if (!lazyContainer.contains(LAZY_INDEX_ELEVATION)) {
                    lazyContainer.set(this, LAZY_INDEX_ELEVATION);
                }
            }
        };
    }

    /**
     * @param elevation the elevation to set
     */
    public void setElevation(Point3D elevation) {
        if (elevation.getX() != 0.0 && elevation.getY() != 0.0 && elevation.getZ() != 0.0) {
            this.lazyContainer.set(elevation, LAZY_INDEX_ELEVATION);
        }
    }


    public void setDocument(DraftDocument doc) {
        super.setDocument(doc);
    }

    /**
     * Not implemented yet
     */

    public void transform(TransformContext context) {

    }

    @Override
    public void toWcs() {
        Extrusion e = this.getExtrusion();
        if (e.compareToNormalVector(0, 0, 1)) {
            return;
        }
        for (LW2DVertex v : vertices) {
            v.transformToWcs(e);
        }
        Extrusion newE = new Extrusion();
        newE.setX(0);
        newE.setY(0);
        newE.setZ(1);
        this.setExtrusion(newE);
    }
}
